import createDOMElement from './createDOMElement';
import mountElement from './mountElement';
import updateElementNode from './updateElementNode';
import diffComponent from './diffComponent';
import { unMountNode } from './utils';

/*
 * @author: yang long
 * @date: 2022-07-19 14:13:39
 * @description: diff 算法
 * @virtualDOM {type}: 虚拟dom
 * @container {type}: 要挂载的 html 节点
 * @oldDOM {type}: 之前页面上旧的 DOM
 */
export default function diff(virtualDOM, container, oldDOM) {
  const oldVirtualDOM = oldDOM && oldDOM._virtualDOM; // 这里的 VirtualDOM 的类型一定不是 组件类型，一定是一个真实dom类型
  const oldComponent = oldVirtualDOM && oldVirtualDOM.component;

  // 判断 oldDOM 是否存在
  if (!oldDOM) {
    // 如果不存在 不需要对比 直接将 Virtual DOM 转换为真实 DOM
    mountElement(virtualDOM, container);
  } else if (oldVirtualDOM && oldVirtualDOM.type === virtualDOM.type) {
    // 新老节点的类型相同的时候
    if (virtualDOM.type === 'text') {
      // 更新内容
      updateTextNode(virtualDOM, oldVirtualDOM, oldDOM);
    } else {
      // 更新元素属性
      updateElementNode(oldDOM, virtualDOM, oldVirtualDOM);
    }

    // 1. 将拥有 key属性的节点存起来
    let keyedElements = {};
    for (let i = 0, len = oldDOM.childNodes; i < len; i++) {
      let oldElement = oldDOM.childNodes[i];
      if (oldElement.nodeType === 1) {
        let key = oldElement.getAttribute('key');
        if (key) {
          keyedElements[key] = oldElement;
        }
      }
    }

    // 判断有没有带 key的元素
    let hasNoKey = Object.keys(keyedElements).length === 0;
    if (hasNoKey) {
      // 更新子元素，新增的节点在这里会被考虑到，没有带key的元素就使用索引的方式更新
      virtualDOM.children.forEach((child, i) => {
        diff(child, oldDOM, oldDOM.childNodes[i]);
      });
    } else {
      // 2. 循环 virtualDOM 子元素，获取key
      virtualDOM.children.forEach((child, i) => {
        let key = child.props.key;
        if (key) {
          let domElement = keyedElements[key];
          if (domElement) {
            // 3. 判断当前位置的元素是不是我们期待的元素
            if (oldDOM.childNodes[i] && oldDOM.childNodes[i] !== domElement) {
              // 不是就需要调整位置，可以直接吧当前元素插入到节点的前面，这样位置就调整好了
              oldDOM.insertBefore(domElement, oldDOM.childNodes[i]);
            }
          } else {
            // 新增元素
            mountElement(child, oldDOM, oldDOM.childNodes[i]);
          }
        }
      });
    }

    // 删除节点处理
    let oldChildNodes = oldDOM.childNodes;
    if (oldChildNodes.length > virtualDOM.children.length) {
      if (hasNoKey) {
        // 没有key就走之前的老办法删除，直接删除少了的
        for (let i = oldChildNodes.length - 1; i > virtualDOM.children.length - 1; i--) {
          unMountNode(oldChildNodes[i]);
        }
      } else {
        // 有key需要通过key来删除
        for (let i = 0, len = oldChildNodes.length; i < len; i++) {
          let oldElement = oldChildNodes[i];
          let oldElementKey = oldElement._virtualDOM.props.key;
          let found = false; // 老key去新的里面找，看看能不能找到，找不到的表示要删除
          for (let j = 0; j < virtualDOM.children.length; j++) {
            if (oldElementKey === virtualDOM.children[j].props.key) {
              found = true;
              break;
            }
          }
          if (!found) {
            unMountNode(oldElement);
          }
        }
      }
    }
  } else if (oldVirtualDOM && oldVirtualDOM.type !== virtualDOM.type && typeof virtualDOM.type !== 'function') {
    // 如果 Virtual DOM 类型不一样 并且 Virtual DOM 不是组件 因为组件要单独进行处理

    // 根据 Virtual DOM 创建真实 DOM 元素
    const newDOMElement = createDOMElement(virtualDOM);
    // 用创建出来的真实 DOM 元素 替换旧的 DOM 元素
    oldDOM.parentNode.replaceChild(newDOMElement, oldDOM);
  } else if (typeof virtualDOM.type === 'function') {
    // 要更新的是组件
    // 1) 组件本身的 virtualDOM 对象 通过它可以获取到组件最新的 props
    // 2) 要更新的组件的实例对象 通过它可以调用组件的生命周期函数 可以更新组件的 props 属性 可以获取到组件返回的最新的 Virtual DOM
    // 3) 要更新的 DOM 象 在更新组件时 需要在已有DOM对象的身上进行修改 实现DOM最小化操作 获取旧的 Virtual DOM 对象
    // 4) 如果要更新的组件和旧组件不是同一个组件 要直接将组件返回的 Virtual DOM 显示在页面中 此时需要 container 做为父级容器
    diffComponent(virtualDOM, oldComponent, oldDOM, container);
  }
}

// 根据新老 virtualDOM 更新 文本类型节点
function updateTextNode(virtualDOM, oldVirtualDOM, oldDOM) {
  // 如果文本节点内容不同
  if (virtualDOM.props.textContent !== oldVirtualDOM.props.textContent) {
    // 更新真实 DOM 对象中的内容
    oldDOM.textContent = virtualDOM.props.textContent;
  }
  // 同步真实 DOM 对应的 Virtual DOM
  oldDOM._virtualDOM = virtualDOM;
}
